package day1;

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;

import javax.annotation.Resource;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@Controller
public class Redis2Controller {

    private static String preFix = "data:";

    private static String redisson_lock = "REDISSON_LOCK";

    @Autowired
    RedisTemplate<String,Object> redisTemplate;

    @Resource
    RedissonClient redissonClient;

    /**
     * 查询列表的接口，不适合做缓存。
     *  原因：不好找，key，并且数据有一点改动，就需要将这个缓存删除。要不然会影响用户查看。
     * @param phone
     */
    @PostMapping("queryList")
    public void queryList(String phone){
    }

    /**
     * 查询详情的接口，就适合做缓存。
     * 这里我们采用，【缓存空对象】来解决【缓存穿透】的问题。
     * @param id
     */
    @PostMapping("queryDetail")
    public Object queryDetail(String id){
        Object o = redisTemplate.opsForValue().get(preFix + id);
        if(o != null){
            //查询到缓存了，返回。
            return o;
        }
        //判断缓存中，是否有这个key。
//        Boolean hasKey = redisTemplate.hasKey(preFix + id);
        //如果缓存中有这个key，并且这个key的value==null，说明这是一个解决【缓存穿透】的【空对象】，所以这一步，直接返回了。
        if(""==o){
            return null;
        }
        //从数据库中获取，最新的值。
        o = new Object();
        //如果数据库中没有这个值，就缓存一个空字符串。
        if(o == null){
            redisTemplate.opsForValue().set(preFix+id,"");
        }
        //存到redis中，并设置过期时间。
        redisTemplate.opsForValue().set(preFix+id,o,5,TimeUnit.MINUTES);
        return o;
    }

    /**
     * 修改的页面。
     * 采用，先更新数据库，再删除缓存。这个是影响最小的一个方案。
     * @param id
     */
    @PostMapping("update")
    public Object update(String id,Object o){
        //TODO：更新数据库中的值。
        System.out.println(o);
        //删除之前的缓存。
        redisTemplate.delete(preFix+id);
        return o;
    }

    //两个方法：解决缓存击穿。【添加一个互斥锁】、【逻辑删除（这个的逻辑等价于 永不过期）】。
    //添加互斥锁：分布式锁。
    public Object test1(String id){
        //从缓存中拿数据。
        Object o = redisTemplate.opsForValue().get(preFix + id);
        //缓存数据为空。
        if(o==null){
            //获取分布式锁。
            RLock lock = redissonClient.getLock(redisson_lock);
            try {
                //尝试获取锁，最多等待【5s】，这个锁自动释放的时间为【10s】
                boolean b = lock.tryLock(5, 10, TimeUnit.SECONDS);
                if(b){
                    //先从缓存中获取一下，看是否已经有缓存了。
                    o = redisTemplate.opsForValue().get(preFix + id);
                    if(o != null){
                        return o;
                    }
                    //获取到锁之后，才开始同步缓存。从数据库中获取缓存。
                    Object o1 =new Object();
                    redisTemplate.opsForValue().set(preFix+id,o1,30,TimeUnit.MINUTES);
                    //释放分布式锁。
                    lock.unlock();
                    return o1;
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }finally {
                //释放锁。
                if (lock.isHeldByCurrentThread() && lock.isLocked()) {
                    //不用添加这个判断，这部分没效果，因为这步，和下面的释放锁不是原子操作。所以一定会出现问题的。
                    lock.unlock();
                    //实际上，直接调用lock.unlock方法释放锁就可以了，它底层通过lua脚本，写了一个原子操作的判断并释放锁的脚本。
                }
            }
        }
        return o;
    }


    /**
     * 思路：添加分布式锁，可以解决缓存击穿的问题。但是它同时也导致了别的线程在获取锁的时候进入等待状态。
     * 所以这里就是另一种写法：逻辑删除。
     *  要点如下：
     *      1、热点数据不设置过期时间。（或者过期时间设置的很大）。
     *      2、方法先从缓存中获取数据，判断是否超过了有效期。
     *          2.1、如果没有超过，直接返回即可。
     *          2.2、如果超过了，尝试获取分布式锁。
     *              2.2.1、如果获取到了分布式锁，就开一个额外的线程，来处理更新数据的事情，这个线程直接将旧数据返回给用户。
     *              2.2.2、如果没有获取到分布式锁，就意味着有别的线程已经开始同步了，这个线程就直接将旧数据返回给用户。
     *  在下面的项目中，热点key的过期时间为 24h。实际上我们希望 30min就去刷新一次热点key。
     *  这里没有使用，新开的线程来更新缓存，因为分布式锁，不能释放别的线程持有的锁。
     */
    public Object test2(String id){
        //按照理论来说，这个一定有值的，热点数据，过期时间设置的很大。
        Long expire = redisTemplate.getExpire(preFix + id,TimeUnit.SECONDS);//这个是24h的时间，也就是 86400s。
        Object o = redisTemplate.opsForValue().get(preFix + id);
        //这个意味着，这条热点数据，已经存在了30min了，需要更新一次了。
        if(expire <= (86400 - 1800)){
            //获取分布式锁。
            RLock lock = redissonClient.getLock(redisson_lock);
            try {
                //等待0s，最大10s自动释放锁。
                boolean b = lock.tryLock(0, 10, TimeUnit.SECONDS);
                if(b){
                    //开始更新缓存。
                    Object o1 = new Object();
                    redisTemplate.opsForValue().set(preFix+id,o1,24,TimeUnit.HOURS);
                    return o1;
                }else{
                    return o;
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }finally {
                //释放锁。
                if(lock.isHeldByCurrentThread() && lock.isLocked()){
                    lock.unlock();
                }
            }
        }
        //没有超过预定的 30 min，不需要刷新缓存。
        return o;
    }




}
